/*
 * Copyright (c) 2020-2022 https://gitee.com/fsfzp888/UVCCapture
 * All rights reserved
 */

#include <QComboBox>
#include <QCoreApplication>
#include <QDir>
#include <QFileDialog>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QImage>
#include <QLabel>
#include <QLineEdit>
#include <QPainter>
#include <QPushButton>
#include <QSizePolicy>
#include <QSpinBox>
#include <QSplitter>
#include <QStatusBar>
#include <QString>
#include <QTime>
#include <QTimer>
#include <QVBoxLayout>
#include <chrono>
#include <ctime>
#include <iomanip>
#include <sstream>

#include "ImageFormats.h"
#include "Logger.h"
#include "VideoCapture.h"
#include "VideoDevice.h"
#include "WebcamWindow.h"

WebcamWindow::WebcamWindow(QWidget *parent)
    : QMainWindow(parent),
      m_stillThread(this),
      m_stillWorker(this),
      m_viewport(new QLabel),
      m_frameMutex(),
      m_frame(),
      m_controlLayout(new QVBoxLayout),
      m_controlGroup(new QGroupBox),
      m_windowLayout(new QHBoxLayout),
      m_windowGroup(new QGroupBox),
      m_startButton(new QPushButton(tr("Turn On"))),
      m_stopButton(new QPushButton(tr("Turn Off"))),
      m_captureButton(new QPushButton(tr("Capture"))),
      m_captureThreeButton(new QPushButton(tr("Capture Three"))),
      m_startRecordVideoButton(new QPushButton(tr("Start Record Video"))),
      m_stopRecordVideoButton(new QPushButton(tr("Stop Record Video"))),
      m_devicesLabel(new QLabel(tr("Devices"))),
      m_devices(new QComboBox),
      m_resolutionsLabel(new QLabel(tr("Resolutions"))),
      m_resolutions(new QComboBox),
      m_directoryLabel(new QLabel(tr("Output Path"))),
      m_directory(new QLineEdit),
      m_browserButton(new QPushButton(tr("Browser"))),
      m_nameLabel(new QLabel(tr("name"))),
      m_name(new QLineEdit),
      m_timeRangeLabel(new QLabel(tr("Hardware trigger video time range(ms)"))),
      m_timeRange(new QSpinBox),
      m_browserDirectoryLayout(new QHBoxLayout),
      m_devicesGroup(new QGroupBox),
      m_devicesLayout(new QVBoxLayout),
      m_vsplitter(new QSplitter),
      m_videoCapture(nullptr),
      m_appDirPath(QDir::currentPath()),
      m_settings("Z Vision Tech", "UVCCapture"),
      m_textSpeaker(new QTextToSpeech(this)),
      m_handleCount(0),
      m_photoCount(0),
      m_stillPhotoCount(0),
      m_isCapturing(false),
      m_isStop(true),
      m_isRecordingVideo(false)
{
    setWindowTitle(tr("Webcam"));
    setWindowFlags(this->windowFlags() | Qt::MaximizeUsingFullscreenGeometryHint);

    m_appDirPath = m_settings.value("mainwindow/directory", m_appDirPath).toString();
    m_directory->setText(m_appDirPath);
    m_directory->setReadOnly(true);
    m_directory->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);

    m_devicesLayout->addWidget(m_devicesLabel);
    m_devicesLayout->addWidget(m_devices);
    m_devicesLayout->addWidget(m_resolutionsLabel);
    m_devicesLayout->addWidget(m_resolutions);

    m_browserDirectoryLayout->addWidget(m_directory, 3);
    m_browserDirectoryLayout->addWidget(m_browserButton, 1);

    m_devicesLayout->addWidget(m_directoryLabel);
    m_devicesLayout->addLayout(m_browserDirectoryLayout);

    m_devicesLayout->addWidget(m_nameLabel);
    m_devicesLayout->addWidget(m_name);

    m_timeRange->setRange(500, 3500);
    m_timeRange->setSingleStep(100);
    m_timeRange->setValue(1800);
    m_devicesLayout->addWidget(m_timeRangeLabel);
    m_devicesLayout->addWidget(m_timeRange);

    m_devicesGroup->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
    m_devicesGroup->setLayout(m_devicesLayout);

    m_controlLayout->addWidget(m_devicesGroup);
    m_controlLayout->addWidget(m_vsplitter);
    m_controlLayout->addWidget(m_captureButton);
    m_controlLayout->addWidget(m_captureThreeButton);
    m_controlLayout->addWidget(m_startRecordVideoButton);
    m_controlLayout->addWidget(m_stopRecordVideoButton);
    m_controlLayout->addWidget(m_startButton);
    m_controlLayout->addWidget(m_stopButton);
    m_controlGroup->setLayout(m_controlLayout);
    m_controlGroup->setMinimumWidth(300);
    m_controlGroup->setMaximumWidth(300);

    m_stopButton->setEnabled(false);
    m_captureButton->setEnabled(false);
    m_captureThreeButton->setEnabled(false);
    m_startRecordVideoButton->setEnabled(false);
    m_stopRecordVideoButton->setEnabled(false);

    m_viewport->setMinimumSize(640, 480);
    m_viewport->setBackgroundRole(QPalette::Base);
    m_viewport->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
    m_viewport->setScaledContents(true);
    m_windowLayout->addWidget(m_viewport);
    m_windowLayout->addWidget(m_controlGroup);
    // m_windowLayout->setSizeConstraint(QLayout::SetFixedSize);
    m_windowGroup->setLayout(m_windowLayout);
    setCentralWidget(m_windowGroup);

    m_statusBar = statusBar();

    m_videoCapture = new VideoCapture([this](unsigned char *data, int len, VideoDevice *device) { processFrame(data, len, device); },
                                      [this](unsigned char *, int, VideoDevice *) {
                                          LOG_INFO("hardware trigger capture still image");
                                          if (!m_isStop && m_isCapturing)
                                          {
                                              //processStillFrame(data, len, device);
                                              incThreeCaptureCnt();
                                          }
                                      });

    auto devicesNames = m_videoCapture->getDevicesNames();
    for (auto &deviceName : devicesNames)
    {
        QString name = QString::fromWCharArray(deviceName.c_str());
        m_devices->addItem(name);
    }

    auto deviceResolutions = m_videoCapture->getActiveDeviceResolutions();
    for (auto &deviceResolution : deviceResolutions)
    {
        QString resolution = QString::fromStdString(deviceResolution);
        m_resolutions->addItem(resolution);
    }
    //if (deviceResolutions.size() > 1)
    //{
    //    m_resolutions->setCurrentIndex(1);
    //}

    connect(m_resolutions, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this,
            static_cast<void (WebcamWindow::*)(int)>(&WebcamWindow::changeResolution));
    connect(m_devices, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this,
            static_cast<void (WebcamWindow::*)(int)>(&WebcamWindow::changeDevice));
    connect(m_startButton, &QPushButton::released, this, &WebcamWindow::startCapture);
    connect(m_stopButton, &QPushButton::released, this, &WebcamWindow::stopCapture);
    connect(m_captureButton, &QPushButton::released, this, &WebcamWindow::incCaptureCnt);
    connect(m_captureThreeButton, &QPushButton::released, this, &WebcamWindow::incThreeCaptureCnt);
    connect(m_startRecordVideoButton, &QPushButton::released, this, &WebcamWindow::startRecordVideo);
    connect(m_stopRecordVideoButton, &QPushButton::released, this, &WebcamWindow::stopRecordVideo);
    connect(m_browserButton, &QPushButton::clicked, this, &WebcamWindow::browse);
    connect(this, SIGNAL(beginRecordVideo()), this, SLOT(onClickStartRecordVideoButton()));
    connect(this, SIGNAL(finishRecordVideo()), this, SLOT(handleFinishRecordVideo()));
    connect(this, SIGNAL(sendStatusBarMessage(QString)), this, SLOT(showStatusBarMessage(QString)));
    connect(this, SIGNAL(sendSpeakMessage(QString)), this, SLOT(speakMessage(QString)));

    m_stillWorker.moveToThread(&m_stillThread);
    connect(&m_stillWorker, SIGNAL(beginRecordVideo()), this, SLOT(startRecordVideo()));
    connect(&m_stillWorker, SIGNAL(finishRecordVideo()), this, SLOT(handleFinishRecordVideo()));
    connect(&m_stillWorker, SIGNAL(sendStatusBarMessage(QString)), this, SLOT(showStatusBarMessage(QString)));
    connect(&m_stillWorker, SIGNAL(sendSpeakMessage(QString)), this, SLOT(speakMessage(QString)));
    connect(&m_stillWorker, SIGNAL(trackExtraImage(int)), this, SLOT(incStillCaptureCnt(int)));
    connect(this, SIGNAL(sendHandleStillImageMessage(QImage &)), &m_stillWorker, SLOT(postImage(QImage &)), Qt::BlockingQueuedConnection);

    m_stillThread.start();
    if (m_textSpeaker->state() == QTextToSpeech::Ready)
    {
        m_textSpeaker->setLocale(QLocale::Chinese);
        m_textSpeaker->setRate(0.0);
        m_textSpeaker->setPitch(1.0);
        m_textSpeaker->setVolume(1.0);
    }
    else
    {
        LOG_ERROR(tr("TTS engine not installed").toLatin1());
    }
    m_timer.start();
}

WebcamWindow::~WebcamWindow()
{
    m_stillThread.terminate();
    m_devices->blockSignals(true);
    m_resolutions->blockSignals(true);
    m_videoCapture->stopCapture();
    delete m_videoCapture;
}

void WebcamWindow::processStillFrame(const unsigned char *data, int len, VideoDevice *device)
{
    if (!device || data == nullptr || len <= 0)
    {
        return;
    }

    VideoDevice::Properties prop = device->getCurrentProperties();

    long width   = prop.width;
    long height  = prop.height;
    m_makeQImage = getQImageMaker(prop.pixelFormat);

    QImage newFrame(m_makeQImage(data, len, width, height));

    emit sendHandleStillImageMessage(newFrame);
}

static std::string getTimeString(bool bLocal = true, bool bIncludeMS = true)
{
    auto tNow = std::chrono::system_clock::now();
    // auto tmNow = std::chrono::system_clock::to_time_t(tNow);
    auto tSeconds = std::chrono::duration_cast<std::chrono::seconds>(tNow.time_since_epoch());
    auto secNow   = tSeconds.count();
    tm tmNow;
    if (bLocal)
    {
        localtime_s(&tmNow, &secNow);
    }
    else
    {
        gmtime_s(&tmNow, &secNow);
    }

    std::ostringstream oss;
    oss << std::put_time(&tmNow, "%Y-%m-%d %H %M %S");
    if (bIncludeMS)
    {
        auto tMilli = std::chrono::duration_cast<std::chrono::milliseconds>(tNow.time_since_epoch());
        auto ms     = tMilli - tSeconds;
        oss << " " << std::setfill('0') << std::setw(3) << ms.count();
    }

    return oss.str();
}

static void writeTextToImage(QImage &img, const QString &text)
{
    QPixmap pix = QPixmap::fromImage(img);
    QPainter painter(&pix);
    painter.begin(&pix);
    painter.setPen(Qt::black);
    QFont font = painter.font();
    font.setPixelSize(18 * img.width() / 640);
    font.setFamily("Microsoft YaHei");
    font.setBold(true);
    painter.setFont(font);
    painter.drawText(pix.rect(), Qt::AlignBottom | Qt::AlignRight, text);
    img = pix.toImage();
}

void WebcamWindow::speakMessage(QString msg)
{
    if (m_textSpeaker->state() == QTextToSpeech::Ready)
    {
        QString name = getUserName();
        m_textSpeaker->say(name);
        m_textSpeaker->say(msg);
    }
    else
    {
        LOG_ERROR("Text to speech engine not in ready state!");
    }
}

void WebcamWindow::showStatusBarMessage(QString msg)
{
    m_statusBar->showMessage(msg, 1500);
}

void WebcamWindow::writeQImageToFile(QImage &img)
{
    QString name = m_name->text();
    if (name.isEmpty())
    {
        name = tr("unknown");
    }
    QString app_dir = m_appDirPath;
    QDir dir;
    if (!dir.exists(app_dir))
    {
        dir.mkpath(app_dir);
    }
    auto pt = getTimeString();
    std::stringstream ss;
    ss << pt << ".jpg";
    std::string filename;
    while (!ss.eof())
    {
        std::string res;
        ss >> res;
        filename += res;
    }
    app_dir = app_dir + "/" + name;
    if (!dir.exists(app_dir))
    {
        dir.mkpath(app_dir);
    }
    app_dir           = app_dir + "/" + filename.c_str();
    QString watermark = name + " " + pt.c_str();
    writeTextToImage(img, watermark);
    img.save(app_dir, "JPG");
    app_dir = app_dir + tr(" has already beed saved.");
    emit sendStatusBarMessage(app_dir);
}

void WebcamWindow::processFrame(const unsigned char *data, int len, VideoDevice *device)
{
    if (!device || data == nullptr || len <= 0)
    {
        return;
    }

    VideoDevice::Properties prop = device->getCurrentProperties();

    long width  = prop.width;
    long height = prop.height;

    m_makeQImage = getQImageMaker(prop.pixelFormat);

    QImage newFrame(m_makeQImage(data, len, width, height));
    m_videoMutex.lock();
    if (m_isRecordingVideo)
    {
        if (isJPEGFormat(prop.pixelFormat))
        {
            m_aviWriter.writeFrame((const char *)data, len, 0);
        }
        else
        {
            QByteArray ba;
            ConvertToJPEGBuf(newFrame, ba);
            m_aviWriter.writeFrame(ba.data(), ba.size(), 0);
        }
    }
    m_videoMutex.unlock();
    m_frameMutex.lock();
    if (m_photoCount)
    {
        writeQImageToFile(newFrame);
        --m_photoCount;
    }
    if (m_stillPhotoCount)
    {
        writeQImageToFile(newFrame);
        --m_stillPhotoCount;
    }
    long vp_width  = m_viewport->width();
    long vp_height = m_viewport->height();
    // m_frame = newFrame.mirrored(device->getCurrentProperties().isFlippedHorizontal, device->getCurrentProperties().isFlippedVertical ||
    // m_isFlipped);
    m_frame = newFrame.scaled(vp_width, vp_height);
    m_frameMutex.unlock();

    QMetaObject::invokeMethod(this, "presentFrame", Qt::QueuedConnection);
}

void WebcamWindow::resizeEvent(QResizeEvent *ev)
{
    m_frameMutex.lock();
    QMainWindow::resizeEvent(ev);
    m_frameMutex.unlock();
}

void WebcamWindow::incCaptureCntCustom(int cnt)
{
    m_frameMutex.lock();
    m_photoCount += cnt;
    m_frameMutex.unlock();
}

void WebcamWindow::incStillCaptureCnt(int cnt)
{
    m_frameMutex.lock();
    m_stillPhotoCount += cnt;
    m_frameMutex.unlock();
}

void WebcamWindow::incCaptureCnt()
{
    emit sendSpeakMessage(tr("save one photos"));
    incCaptureCntCustom(1);
}

void WebcamWindow::incThreeCaptureCnt()
{
    emit sendSpeakMessage(tr("save three photos"));
    incCaptureCntCustom(1);
    incStillCaptureCnt(2);
}

void WebcamWindow::presentFrame()
{
    m_frameMutex.lock();
    m_viewport->setPixmap(QPixmap::fromImage(m_frame));
    m_frameMutex.unlock();
    // adjustSize();
    m_viewport->repaint();
}

void WebcamWindow::changeResolution(int resolutionNum)
{
    bool wasCapturing = m_isCapturing;
    stopCapture();
    m_videoCapture->changeActiveDeviceResolution(resolutionNum);

    // adjustSize();
    if (wasCapturing)
    {
        startCapture();
    }
}

void WebcamWindow::changeDevice(int deviceNum)
{
    bool wasCapturing = m_isCapturing;
    stopCapture();

    m_videoCapture->changeActiveDevice(deviceNum);

    m_resolutions->clear();
    auto deviceResolutions = m_videoCapture->getActiveDeviceResolutions();
    for (auto &deviceResolution : deviceResolutions)
    {
        QString resolution = QString::fromStdString(deviceResolution);
        m_resolutions->addItem(resolution);
    }

    if (wasCapturing)
    {
        startCapture();
    }
}

void WebcamWindow::onClockStopRecordVideoButton()
{
    handleFinishRecordVideo();
    emit sendStatusBarMessage(tr("Video finish record."));
    emit sendSpeakMessage(tr("finish record video"));
}

void WebcamWindow::handleFinishRecordVideo()
{
    // signal may be emit multiple times
    m_videoMutex.lock();
    if (m_isRecordingVideo)
    {
        m_aviWriter.close();
        m_isRecordingVideo = false;
        m_startRecordVideoButton->setEnabled(true);
        m_stopRecordVideoButton->setEnabled(false);
        LOG_INFO("Close AVI video file");
    }
    m_videoMutex.unlock();
}

void WebcamWindow::startCapture()
{
    m_isStop = false;
    m_startButton->setEnabled(false);
    m_stopButton->setEnabled(true);
    m_captureButton->setEnabled(true);
    m_captureThreeButton->setEnabled(true);
    m_startRecordVideoButton->setEnabled(true);
    m_stopRecordVideoButton->setEnabled(false);

    if (m_videoCapture->startCapture())
    {
        m_isCapturing = true;
    }
}

void WebcamWindow::stopCapture()
{
    m_isStop = true;
    m_startButton->setEnabled(true);
    m_stopButton->setEnabled(false);
    m_captureButton->setEnabled(false);
    m_captureThreeButton->setEnabled(false);
    m_startRecordVideoButton->setEnabled(false);
    m_stopRecordVideoButton->setEnabled(false);

    if (m_videoCapture->stopCapture())
    {
        m_isCapturing = false;
    }
    m_frame.fill(Qt::GlobalColor::white);
    presentFrame();
    if (m_isRecordingVideo)
    {
        emit finishRecordVideo();
    }
}

QString str2qstr(const std::string str)
{
    return QString::fromLocal8Bit(str.data());
}

std::string qstr2str(const QString qstr)
{
    QByteArray cdata = qstr.toLocal8Bit();
    return std::string(cdata);
}

void WebcamWindow::onClickStartRecordVideoButton()
{
    startRecordVideo();
    emit sendSpeakMessage(tr("start record video"));
}

void WebcamWindow::startRecordVideo()
{
    m_startRecordVideoButton->setEnabled(false);
    m_stopRecordVideoButton->setEnabled(true);
    QString name = m_name->text();
    if (name.isEmpty())
    {
        name = tr("unknown");
    }
    QString app_dir = m_appDirPath;
    QDir dir;
    if (!dir.exists(app_dir))
    {
        dir.mkpath(app_dir);
    }
    auto pt = getTimeString();
    std::stringstream ss;
    ss << pt << ".avi";
    std::string filename;
    while (!ss.eof())
    {
        std::string res;
        ss >> res;
        filename += res;
    }
    app_dir = app_dir + "/" + name;
    if (!dir.exists(app_dir))
    {
        dir.mkpath(app_dir);
    }
    app_dir                      = app_dir + "/" + filename.c_str();
    auto device                  = m_videoCapture->getActiveDevice();
    VideoDevice::Properties prop = device->getCurrentProperties();

    long width  = prop.width;
    long height = prop.height;
    filename    = qstr2str(app_dir);
    if (!m_aviWriter.open(filename.c_str()))
    {
        LOG_ERROR("Fail to open AVI file %s to write", app_dir.toStdString().c_str());
        m_startRecordVideoButton->setEnabled(true);
        return;
    }
    LOG_INFO("Open AVI video file %s", filename.c_str());
    double real_fps = m_fps;
    if (real_fps < 10)
    {
        real_fps = 10.0;
    }
    m_aviWriter.setVideo(width, height, real_fps, "MJPG");
    m_isRecordingVideo = true;
    app_dir            = app_dir + tr(" start recording.");
    emit sendStatusBarMessage(app_dir);
    QString speak_msg = tr(" start recording.");
    emit sendSpeakMessage(speak_msg);
}

void WebcamWindow::stopRecordVideo()
{
    emit finishRecordVideo();
    QString speak_msg = tr("Video finish record.");
    emit sendSpeakMessage(speak_msg);
    emit sendStatusBarMessage(speak_msg);
}

void WebcamWindow::browse()
{
    QString directory = QFileDialog::getExistingDirectory(this, tr("Get output directory"), QDir::currentPath());
    if (!directory.isEmpty())
    {
        m_appDirPath = directory;
        m_directory->setText(directory);
        m_settings.setValue("mainwindow/directory", m_appDirPath);
    }
}

QString WebcamWindow::getUserName() const
{
    return m_name->text();
}

int WebcamWindow::getTimeRange() const
{
    return m_timeRange->value();
}

StillImageWorker::StillImageWorker(WebcamWindow *win) : m_win(win) {}

StillImageWorker::~StillImageWorker() noexcept {}

void StillImageWorker::postImage(QImage &img)
{
    m_stillImageQueueMtx.lock();
    m_stillImageQueue.push_back(std::move(img));
    m_stillImageQueueMtx.unlock();
    QTimer::singleShot(m_win->getTimeRange(), this, SLOT(handleStillImageQueue()));
}

void StillImageWorker::handleStillImageQueue()
{
    m_stillImageQueueMtx.lock();
    if (m_stillImageQueue.size())
    {
        bool is_need_record_video = false;
        if (m_stillImageQueue.size() > 1)
        {
            is_need_record_video = true;
        }
        // only save the first image
        if (is_need_record_video)
        {
            if (m_win->isRecordingVideo())
            {
                emit finishRecordVideo();
                emit sendSpeakMessage(tr("finish record video"));
            }
            else
            {
                emit beginRecordVideo();
            }
        }
        else
        {
            QImage newFrame = std::move(m_stillImageQueue[0]);
            writeQImageToFile(newFrame);
            emit sendSpeakMessage(tr("save three photos"));
            emit trackExtraImage(2);
        }
        m_stillImageQueue.clear();
    }
    m_stillImageQueueMtx.unlock();
}

void StillImageWorker::writeQImageToFile(QImage &img)
{
    QString name = m_win->getUserName();
    if (name.isEmpty())
    {
        name = tr("unknown");
    }
    QString app_dir = m_win->getAppDirPath();
    QDir dir;
    if (!dir.exists(app_dir))
    {
        dir.mkpath(app_dir);
    }
    auto pt = getTimeString();
    std::stringstream ss;
    ss << pt << ".jpg";
    std::string filename;
    while (!ss.eof())
    {
        std::string res;
        ss >> res;
        filename += res;
    }
    app_dir = app_dir + "/" + name;
    if (!dir.exists(app_dir))
    {
        dir.mkpath(app_dir);
    }
    app_dir           = app_dir + "/" + filename.c_str();
    QString watermark = name + " " + pt.c_str();
    writeTextToImage(img, watermark);
    img.save(app_dir, "JPG");
    app_dir = app_dir + tr(" has already beed saved.");
    emit sendStatusBarMessage(app_dir);
}
